﻿// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================
namespace Microsoft.Xrm.Sdk
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics.CodeAnalysis;
	using System.Globalization;
	using System.Linq;
    using System.Reflection;
	using System.Runtime.Serialization;
	using Microsoft.Xrm.Sdk.Temporary;

	/// <summary>
	/// Represents an instance of an entity.
	/// </summary>
	[SuppressMessage("Microsoft.Security", "CA9881:ClassesShouldBeSealed", Justification = "This class need to be instantiated by clients and be able to derive from it.")]
	[DataContract(Name = "Entity", Namespace = XmlNamespaces.V5.Contracts)]
	public class Entity : IExtensibleDataObject
	{
		public Entity()
			: this(null)
		{
		}

		public Entity(string entityName)
		{
			_logicalName = entityName;
		}

		/// <summary>
		/// Specifies the name of the entity.
		/// </summary>
		[DataMember]
		public string LogicalName
		{
			get
			{
				return _logicalName;
			}
			set
			{
				CheckIsReadOnly("LogicalName");
				_logicalName = value;
			}
		}

		/// <summary>
		/// Specifies the unique id of the entity;
		/// </summary>
		[DataMember]
		public virtual Guid Id
		{
			get
			{
				return _id;
			}
			set
			{
				if (_id != Guid.Empty)
				{
					CheckIsReadOnly("Id");
				}

				_id = value;
			}
		}

		/// <summary>
		/// Specifies a collection of attribute name/value pairs for the entity.
		/// </summary>
		[DataMember]
		public AttributeCollection Attributes
		{
			get
			{
				if (_attributes == null)
				{
					_attributes = new AttributeCollection();
				}
				return _attributes;
			}
			set
			{
				_attributes = value;
			}
		}

		[DataMember]
		public EntityState? EntityState
		{
			get
			{
				return _entityState;
			}
			set
			{
				CheckIsReadOnly("EntityState");
				_entityState = value;
			}
		}

		[DataMember]
		public FormattedValueCollection FormattedValues
		{
			get
			{
				if (_formattedValues == null)
				{
					_formattedValues = new FormattedValueCollection();
				}
				return _formattedValues;
			}
			internal set
			{
				_formattedValues = value;
			}
		}

		[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Member and conflicting method differences are obvious.")]
		[DataMember]
		public RelatedEntityCollection RelatedEntities
		{
			get
			{
				if (_relatedEntities == null)
				{
					_relatedEntities = new RelatedEntityCollection();
					_relatedEntities.IsReadOnly = IsReadOnly;
				}
				return _relatedEntities;
			}
			internal set
			{
				CheckIsReadOnly("RelatedEntities");
				_relatedEntities = value;
			}
		}

		/// <summary>
		/// Gets or sets the value of an attribute in an entity
		/// </summary>
		/// <param name="attributeName">logical name of attribute</param>
		/// <returns>value of the attribute</returns>
		public object this[string attributeName]
		{
			get
			{
				return this.Attributes[attributeName];
			}
			set
			{
				this.Attributes[attributeName] = value;
			}
		}

		#region utility methods

		/// <summary>
		/// Checks if value for an attribute is specified
		/// </summary>
		/// <param name="attributeName">logical name of attribute</param>
		/// <returns>true if the attribute has value, otherwise false</returns>
		public bool Contains(string attributeName)
		{
			return this.Attributes.Contains(attributeName);
		}

		/// <summary>
		/// Creates an early bound instance of current entity, and copies members to it
		/// If T = Entity, the instance will be copied to a late bound instance
		/// </summary>
		/// <typeparam name="T">Early bound type</typeparam>
		/// <returns>Early bound instance of entity</returns>
		public T ToEntity<T>() where T : Entity
		{
			if (typeof(T) == typeof(Entity))
			{
				Entity copy = new Entity();
				this.ShallowCopyTo(copy);
				return copy as T;
			}

			if (string.IsNullOrWhiteSpace(_logicalName))
			{
				throw new NotSupportedException("LogicalName must be set before calling ToEntity()");
			}

			string targetLogicalName = null;
            System.Collections.IEnumerable logicalNameAttributes = null;
#if SILVERLIGHT
			logicalNameAttributes = typeof(T).GetCustomAttributes(typeof(Client.EntityLogicalNameAttribute), true);
#else
            logicalNameAttributes = typeof(T).GetTypeInfo().GetCustomAttributes<Client.EntityLogicalNameAttribute>(true);
#endif
            if (logicalNameAttributes != null)
			{
				foreach (Client.EntityLogicalNameAttribute attribute in logicalNameAttributes)
				{
					targetLogicalName = attribute.LogicalName;
					break;
				}
			}

			if (string.IsNullOrWhiteSpace(targetLogicalName))
			{
				throw new NotSupportedException("Cannot convert to type that is does not have EntityLogicalNameAttribute");
			}

			if (_logicalName != targetLogicalName)
			{
				throw new NotSupportedException(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Cannot convert entity {0} to {1}", _logicalName, targetLogicalName));
			}

			T proxy = (T)Activator.CreateInstance(typeof(T));
			this.ShallowCopyTo(proxy);

			return proxy;
		}

		/// <summary>
		/// Creates a reference for the current entity instance.
		/// </summary>
		public EntityReference ToEntityReference()
		{
			return new EntityReference(this.LogicalName, this.Id);
		}

		#endregion

		#region internal/private utility methods
		/// <summary>
		/// Performs a shallow copy of members
		/// </summary>
		/// <param name="target"></param>
		internal void ShallowCopyTo(Entity target)
		{
			if (target != null && target != this)
			{
				// This code assumes that target is a newly created Entity and that Id is not set.
				target.Id = this.Id;

				target.SetLogicalNameInternal(this.LogicalName);
				target.SetEntityStateInternal(this.EntityState);
				target.SetRelatedEntitiesInternal(this.RelatedEntities);
				target.Attributes = this.Attributes;
				target.FormattedValues = this.FormattedValues;
				target.ExtensionData = this.ExtensionData;
			}
		}

		#endregion

		public virtual T GetAttributeValue<T>(string attributeLogicalName)
		{
			var value = GetAttributeValue(attributeLogicalName);

			return value != null ? (T)value : default(T);
		}

		private object GetAttributeValue(string attributeLogicalName)
		{
			if (string.IsNullOrWhiteSpace(attributeLogicalName)) throw new ArgumentNullException("attributeLogicalName");

			return Contains(attributeLogicalName) ? this[attributeLogicalName] : null;
		}

		protected virtual void SetAttributeValue(string attributeLogicalName, object value)
		{
			if (string.IsNullOrWhiteSpace(attributeLogicalName)) throw new ArgumentNullException("attributeLogicalName");

			this[attributeLogicalName] = value;
		}

		protected virtual string GetFormattedAttributeValue(string attributeLogicalName)
		{
			if (string.IsNullOrWhiteSpace(attributeLogicalName)) throw new ArgumentNullException("attributeLogicalName");

			return FormattedValues.Contains(attributeLogicalName)
				? FormattedValues[attributeLogicalName]
				: null;
		}

		protected virtual TEntity GetRelatedEntity<TEntity>(string relationshipSchemaName, EntityRole? primaryEntityRole)
			where TEntity : Entity
		{
			if (string.IsNullOrWhiteSpace(relationshipSchemaName)) throw new ArgumentNullException("relationshipSchemaName");

			var key = new Relationship(relationshipSchemaName) { PrimaryEntityRole = primaryEntityRole };

			return RelatedEntities.Contains(key)
				? (TEntity)RelatedEntities[key].Entities.FirstOrDefault()
				: null;
		}

		protected virtual void SetRelatedEntity<TEntity>(string relationshipSchemaName, EntityRole? primaryEntityRole, TEntity entity)
			where TEntity : Entity
		{
			if (string.IsNullOrWhiteSpace(relationshipSchemaName)) throw new ArgumentNullException("relationshipSchemaName");
			if (entity != null && string.IsNullOrWhiteSpace(entity.LogicalName)) throw new ArgumentException("The entity is missing a value for the 'LogicalName' property.", "entity");

			var key = new Relationship(relationshipSchemaName) { PrimaryEntityRole = primaryEntityRole };

			var collection = entity != null
				? new EntityCollection(new[] { entity }) { EntityName = entity.LogicalName }
				: null;

			if (collection != null)
			{
				RelatedEntities[key] = collection;
			}
			else
			{
				RelatedEntities.Remove(key);
			}
		}

		protected virtual IEnumerable<TEntity> GetRelatedEntities<TEntity>(string relationshipSchemaName, EntityRole? primaryEntityRole)
			where TEntity : Entity
		{
			if (string.IsNullOrWhiteSpace(relationshipSchemaName)) throw new ArgumentNullException("relationshipSchemaName");

			var key = new Relationship(relationshipSchemaName) { PrimaryEntityRole = primaryEntityRole };

			return RelatedEntities.Contains(key)
				? RelatedEntities[key].Entities.Cast<TEntity>()
				: null;
		}

		protected virtual void SetRelatedEntities<TEntity>(string relationshipSchemaName, EntityRole? primaryEntityRole, IEnumerable<TEntity> entities)
				where TEntity : Entity
		{
			if (string.IsNullOrWhiteSpace(relationshipSchemaName)) throw new ArgumentNullException("relationshipSchemaName");

			if (entities != null && entities.Any(entity => string.IsNullOrWhiteSpace(entity.LogicalName)))
			{
				throw new ArgumentException("An entity is missing a value for the 'LogicalName' property.", "entities");
			}

			var key = new Relationship(relationshipSchemaName) { PrimaryEntityRole = primaryEntityRole };

			if (entities != null)
			{
				var collection = new EntityCollection() { EntityName = entities.First().LogicalName };
				foreach (Entity e in entities)
				{
					collection.Entities.Add(e);
				}
				RelatedEntities[key] = collection;
			}
			else
			{
				RelatedEntities.Remove(key);
			}
		}

		#region internal members
		/// <summary>
		/// Indicates that the entity is attached to an <see cref="OrganizationServiceContext"/>.
		/// </summary>
		internal bool IsReadOnly
		{
			get
			{
				return _isReadOnly;
			}
			set
			{
				_isReadOnly = value;
				RelatedEntities.IsReadOnly = value;
			}
		}

		internal void SetLogicalNameInternal(string logicalName)
		{
			_logicalName = logicalName;
		}

		internal void SetEntityStateInternal(EntityState? entityState)
		{
			_entityState = entityState;
		}

		internal void SetRelatedEntitiesInternal(RelatedEntityCollection relatedEntities)
		{
			_relatedEntities = relatedEntities;
		}
		#endregion

		#region private members
		private string _logicalName;
		private Guid _id;
		private AttributeCollection _attributes;
		private EntityState? _entityState;
		private FormattedValueCollection _formattedValues;
		private RelatedEntityCollection _relatedEntities;
		internal bool _isReadOnly;

		private void CheckIsReadOnly(string propertyName)
		{
			if (IsReadOnly) throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "The entity is read-only and the '{0}' property cannot be modified. Use the context to update the entity instead.", propertyName));
		}
		#endregion


		#region IExtensibleDataObject Implementation
		private ExtensionDataObject ExtensionDataObject;
		public ExtensionDataObject ExtensionData
		{
			get
			{
				return ExtensionDataObject;
			}
			set
			{
				ExtensionDataObject = value;
			}
		}
		#endregion IExtensibleDataObject Implementation
	}
}
